import fs from '@ohos.file.fs';
import JSZip from '@ohos/jszip';

import ZipStream from '../utils/zip-stream';
import StreamBuf from '../utils/stream-buf';

import XmlStream from '../utils/xml-stream';

import StylesXform from './xform/style/styles-xform';

import CoreXform from './xform/core/core-xform';
import SharedStringsXform from './xform/strings/shared-strings-xform';
import RelationshipsXform from './xform/core/relationships-xform';
import ContentTypesXform from './xform/core/content-types-xform';
import AppXform from './xform/core/app-xform';
import WorkbookXform from './xform/book/workbook-xform';
import WorksheetXform from './xform/sheet/worksheet-xform';
import DrawingXform from './xform/drawing/drawing-xform';
import TableXform from './xform/table/table-xform';
import PivotCacheRecordsXform from './xform/pivot-table/pivot-cache-records-xform';
import PivotCacheDefinitionXform from './xform/pivot-table/pivot-cache-definition-xform';
import PivotTableXform from './xform/pivot-table/pivot-table-xform';
import CommentsXform from './xform/comment/comments-xform';
import VmlNotesXform from './xform/comment/vml-notes-xform';

import theme1Xml from './xml/theme1';
import RelType from './rel-type';
import { LogUtil } from '../utils/log-util'

function fsReadFileAsync(filename, options) {
    return new Promise((resolve, reject) => {
        let stat = fs.statSync(filename);
        let file = fs.openSync(filename);
        let length = stat.size;
        let buf = new ArrayBuffer(length);
        fs.read(file.fd, buf, options, (error, readLen) => {
            if (error) {
                reject(error);
            } else {
                resolve(buf);
            }
        });
    });
}

class XLSX {
    constructor(workbook) {
        this.workbook = workbook;
    }

    async readFile(filename, options) {
        if (!(fs.accessSync(filename))) {
            throw new Error(`File not found: ${filename}`);
        }
        try {
            let length = fs.statSync(filename).size
            let buf = new ArrayBuffer(length);
            let fd = fs.openSync(filename, fs.OpenMode.READ_ONLY).fd;
            fs.readSync(fd, buf)
            fs.closeSync(fd)
            const workbook = await this.read(buf, options);
            return workbook;
        } catch (error) {
            fs.closeSync(fd)
            LogUtil.error("readFile error:" + JSON.stringify(error))
            throw error;
        }
    }


    parseRels(xmlstr) {
        const xform = new RelationshipsXform();
        return xform.parseXml(xmlstr);
    }

    parseWorkbook(xmlstr) {
        const xform = new WorkbookXform();
        return xform.parseXml(xmlstr);
    }

    parseSharedStrings(xmlstr) {
        const xform = new SharedStringsXform();
        return xform.parseXml(xmlstr);
    }

    reconcile(model, options) {
        const workbookXform = new WorkbookXform();
        const worksheetXform = new WorksheetXform(options);
        const drawingXform = new DrawingXform();
        const tableXform = new TableXform();

        workbookXform.reconcile(model);

        // reconcile drawings with their rels
        const drawingOptions = {
            media: model.media,
            mediaIndex: model.mediaIndex,
        };
        Object.keys(model.drawings).forEach(name => {
            const drawing = model.drawings[name];
            const drawingRel = model.drawingRels[name];
            if (drawingRel) {
                drawingOptions.rels = drawingRel.reduce((o, rel) => {
                    o[rel.Id] = rel;
                    return o;
                }, {});
                (drawing?.anchors || []).forEach(anchor => {
                    const hyperlinks = anchor.picture && anchor.picture.hyperlinks;
                    if (hyperlinks && drawingOptions.rels[hyperlinks.rId]) {
                        hyperlinks.hyperlink = drawingOptions.rels[hyperlinks.rId].Target;
                        delete hyperlinks.rId;
                    }
                });
                drawingXform.reconcile(drawing, drawingOptions);
            }
        });

        // reconcile tables with the default styles
        const tableOptions = {
            styles: model.styles,
        };
        Object.values(model.tables).forEach(table => {
            tableXform.reconcile(table, tableOptions);
        });

        const sheetOptions = {
            styles: model.styles,
            sharedStrings: model.sharedStrings,
            media: model.media,
            mediaIndex: model.mediaIndex,
            date1904: model.properties && model.properties.date1904,
            drawings: model.drawings,
            comments: model.comments,
            tables: model.tables,
            vmlDrawings: model.vmlDrawings,
        };
        model.worksheets.forEach(worksheet => {
            worksheet.relationships = model.worksheetRels[worksheet.sheetNo];
            worksheetXform.reconcile(worksheet, sheetOptions);
        });

        // delete unnecessary parts
        delete model.worksheetHash;
        delete model.worksheetRels;
        delete model.globalRels;
        delete model.sharedStrings;
        delete model.workbookRels;
        delete model.sheetDefs;
        delete model.styles;
        delete model.mediaIndex;
        delete model.drawings;
        delete model.drawingRels;
        delete model.vmlDrawings;
    }

    async _processWorksheetEntry(xmlstr, model, sheetNo, options, path) {
        const xform = new WorksheetXform(options);
        const worksheet = await xform.parseXml(xmlstr);
        worksheet.sheetNo = sheetNo;
        model.worksheetHash[path] = worksheet;
        model.worksheets.push(worksheet);
    }

    async _processCommentEntry(xmlstr, model, name) {
        const xform = new CommentsXform();
        const comments = await xform.parseXml(xmlstr);
        model.comments[`../${name}.xml`] = comments;
    }

    async _processTableEntry(xmlstr, model, name) {
        const xform = new TableXform();
        const table = await xform.parseXml(xmlstr);
        model.tables[`../tables/${name}.xml`] = table;
    }

    async _processWorksheetRelsEntry(xmlstr, model, sheetNo) {
        const xform = new RelationshipsXform();
        const relationships = await xform.parseXml(xmlstr);
        model.worksheetRels[sheetNo] = relationships;
    }

    async _processMediaEntry(mediaArrayBuffer, model, filename) {
        const lastDot = filename.lastIndexOf('.');
        // if we can't determine extension, ignore it
        if (lastDot >= 1) {
            const extension = filename.substr(lastDot + 1);
            const name = filename.substr(0, lastDot);
            model.mediaIndex[filename] = model.media.length;
            model.mediaIndex[name] = model.media.length;
            const medium = {
                type: 'image',
                name,
                extension,
                buffer: mediaArrayBuffer,
            };
            model.media.push(medium);
        }
    }

    async _processDrawingEntry(xmlStr, model, name) {
        const xform = new DrawingXform();
        const drawing = await xform.parseXml(xmlStr);
        model.drawings[name] = drawing;
    }

    async _processDrawingRelsEntry(xmlStr, model, name) {
        const xform = new RelationshipsXform();
        const relationships = await xform.parseXml(xmlStr);
        model.drawingRels[name] = relationships;
    }

    async _processVmlDrawingEntry(entry, model, name) {
        const xform = new VmlNotesXform();
        const vmlDrawing = await xform.parseXml(entry);
        model.vmlDrawings[`../drawings/${name}.vml`] = vmlDrawing;
    }

    async _processThemeEntry(xmlStr, model, name) {
        model.themes[name] = xmlStr;
    }

    /**
     * @deprecated since version 4.0. You should use `#read` instead. Please follow upgrade instruction: https://github.com/exceljs/exceljs/blob/master/UPGRADE-4.0.md
     */
    createInputStream() {
        throw new Error(
            '`XLSX#createInputStream` is deprecated. You should use `XLSX#read` instead. This method will be removed in version 5.0. Please follow upgrade instruction: https://github.com/exceljs/exceljs/blob/master/UPGRADE-4.0.md'
        );
    }

    /**
     * note：目前不支持流的读取，jszip不支持流的加载，@ohos.xml也不支持流式数据的解析
     * @param buffer
     * @param options
     * @returns
     */
    async read(buffer, options) {
        return this.load(buffer, options);
    }

    async load(buffer, options) {

        const model = {
            worksheets: [],
            worksheetHash: {},
            worksheetRels: [],
            themes: {},
            media: [],
            mediaIndex: {},
            drawings: {},
            drawingRels: {},
            comments: {},
            tables: {},
            vmlDrawings: {},
        };

        const zip = await JSZip.loadAsync(buffer);

        for (const entry of Object.values(zip.files)) {
            if (!entry.dir) {
                const entryName = entry.name
                switch (entryName) {
                    case '_rels/.rels':
                        const relsXmlstr = await zip.file(entryName).async("string")
                        model.globalRels = await this.parseRels(relsXmlstr);
                        break;

                    case 'xl/workbook.xml': {
                        const workbookXmlstr = await zip.file(entryName).async("string")
                        const workbook = await this.parseWorkbook(workbookXmlstr);
                        model.sheets = workbook.sheets;
                        model.definedNames = workbook.definedNames;
                        model.views = workbook.views;
                        model.properties = workbook.properties;
                        model.calcProperties = workbook.calcProperties;
                        break;
                    }

                    case 'xl/_rels/workbook.xml.rels':
                        const wbRelsXmlstr = await zip.file(entryName).async("string")
                        model.workbookRels = await this.parseRels(wbRelsXmlstr);
                        break;

                    case 'xl/sharedStrings.xml':
                        const sharedXmlstr = await zip.file(entryName).async("string")
                        model.sharedStrings = new SharedStringsXform();
                        await model.sharedStrings.parseXml(sharedXmlstr);
                        break;

                    case 'xl/styles.xml':
                        const stylesXmlstr = await zip.file(entryName).async("string")
                        model.styles = new StylesXform();
                        await model.styles.parseXml(stylesXmlstr);
                        break;

                    case 'docProps/app.xml': {
                        const appXmlstr = await zip.file(entryName).async("string")
                        const appXform = new AppXform();
                        const appProperties = await appXform.parseXml(appXmlstr);
                        model.company = appProperties.company;
                        model.manager = appProperties.manager;
                        break;
                    }

                    case 'docProps/core.xml': {
                        const coreXmlstr = await zip.file(entryName).async("string")
                        const coreXform = new CoreXform();
                        const coreProperties = await coreXform.parseXml(coreXmlstr);
                        Object.assign(model, coreProperties);
                        break;
                    }

                    default: {
                        let match = entryName.match(/xl\/worksheets\/sheet(\d+)[.]xml/);
                        if (match) {
                            const worksheetXmlstr = await zip.file(entryName).async("string")
                            await this._processWorksheetEntry(worksheetXmlstr, model, match[1], options, entryName);
                            break;
                        }
                        match = entryName.match(/xl\/worksheets\/_rels\/sheet(\d+)[.]xml.rels/);
                        if (match) {
                            const worksheetRelsXmlstr = await zip.file(entryName).async("string")
                            await this._processWorksheetRelsEntry(worksheetRelsXmlstr, model, match[1]);
                            break;
                        }
                        match = entryName.match(/xl\/theme\/([a-zA-Z0-9]+)[.]xml/);
                        if (match) {
                            const themeXmlstr = await zip.file(entryName).async("string")
                            await this._processThemeEntry(themeXmlstr, model, match[1]);
                            break;
                        }
                        match = entryName.match(/xl\/media\/([a-zA-Z0-9]+[.][a-zA-Z0-9]{3,4})$/);
                        if (match) {
                            const mediaArrayBuffer = await zip.file(entryName).async("arraybuffer")
                            await this._processMediaEntry(mediaArrayBuffer, model, match[1]);
                            break;
                        }
                        match = entryName.match(/xl\/drawings\/([a-zA-Z0-9]+)[.]xml/);
                        if (match) {
                            const drawingsXmlstr = await zip.file(entryName).async("string")
                            await this._processDrawingEntry(drawingsXmlstr, model, match[1]);
                            break;
                        }
                        match = entryName.match(/xl\/(comments\d+)[.]xml/);
                        if (match) {
                            const commentsXmlstr = await zip.file(entryName).async("string")
                            await this._processCommentEntry(commentsXmlstr, model, match[1]);
                            break;
                        }
                        match = entryName.match(/xl\/tables\/(table\d+)[.]xml/);
                        if (match) {
                            const tableXmlstr = await zip.file(entryName).async("string")
                            await this._processTableEntry(tableXmlstr, model, match[1]);
                            break;
                        }
                        match = entryName.match(/xl\/drawings\/_rels\/([a-zA-Z0-9]+)[.]xml[.]rels/);
                        if (match) {
                            const drawingsRelsXmlstr = await zip.file(entryName).async("string")
                            await this._processDrawingRelsEntry(drawingsRelsXmlstr, model, match[1]);
                            break;
                        }
                        match = entryName.match(/xl\/drawings\/(vmlDrawing\d+)[.]vml/);
                        if (match) {
                            const vmlDrawingVmlstr = await zip.file(entryName).async("string")
                            await this._processVmlDrawingEntry(vmlDrawingVmlstr, model, match[1]);
                            break;
                        }
                    }
                }
            }
        }

        this.reconcile(model, options);

        // apply model
        this.workbook.model = model;
        return this.workbook;
    }

    // =========================================================================
    // Write

    async addMedia(zip, model) {
        await Promise.all(
            model.media.map(async medium => {
                if (medium.type === 'image') {
                    const filename = `xl/media/${medium.name}.${medium.extension}`;
                    if (medium.filename) {
                        const data = await fsReadFileAsync(medium.filename);
                        return zip.append(data, { name: filename });
                    }
                    if (medium.buffer) {
                        return zip.append(medium.buffer, { name: filename });
                    }
                    if (medium.base64) {
                        const dataimg64 = medium.base64;
                        const content = dataimg64.substring(dataimg64.indexOf(',') + 1);
                        return zip.append(content, { name: filename, base64: true });
                    }
                }
                throw new Error('Unsupported media');
            })
        );
    }

    addDrawings(zip, model) {
        const drawingXform = new DrawingXform();
        const relsXform = new RelationshipsXform();

        model.worksheets.forEach(worksheet => {
            const { drawing } = worksheet;
            if (drawing) {
                drawingXform.prepare(drawing, {});
                let xml = drawingXform.toXml(drawing);
                zip.append(xml, { name: `xl/drawings/${drawing.name}.xml` });

                xml = relsXform.toXml(drawing.rels);
                zip.append(xml, { name: `xl/drawings/_rels/${drawing.name}.xml.rels` });
            }
        });
    }

    addTables(zip, model) {
        const tableXform = new TableXform();

        model.worksheets.forEach(worksheet => {
            const { tables } = worksheet;
            tables.forEach(table => {
                tableXform.prepare(table, {});
                const tableXml = tableXform.toXml(table);
                zip.append(tableXml, { name: `xl/tables/${table.target}` });
            });
        });
    }

    addPivotTables(zip, model) {
        if (!model.pivotTables.length) {
            return;
        }

        const pivotTable = model.pivotTables[0];

        const pivotCacheRecordsXform = new PivotCacheRecordsXform();
        const pivotCacheDefinitionXform = new PivotCacheDefinitionXform();
        const pivotTableXform = new PivotTableXform();
        const relsXform = new RelationshipsXform();

        // pivot cache records
        // --------------------------------------------------
        // copy of the source data.
        //
        // Note: cells in the columns of the source data which are part
        // of the "rows" or "columns" of the pivot table configuration are
        // replaced by references to their __cache field__ identifiers.
        // See "pivot cache definition" below.

        let xml = pivotCacheRecordsXform.toXml(pivotTable);
        zip.append(xml, { name: 'xl/pivotCache/pivotCacheRecords1.xml' });

        // pivot cache definition
        // --------------------------------------------------
        // cache source (source data):
        //    ref="A1:E7" on sheet="Sheet1"
        // cache fields:
        //    - 0: "A" (a1, a2, a3)
        //    - 1: "B" (b1, b2)
        //    - ...

        xml = pivotCacheDefinitionXform.toXml(pivotTable);
        zip.append(xml, { name: 'xl/pivotCache/pivotCacheDefinition1.xml' });

        xml = relsXform.toXml([
            {
                Id: 'rId1',
                Type: XLSX.RelType.PivotCacheRecords,
                Target: 'pivotCacheRecords1.xml',
            },
        ]);
        zip.append(xml, { name: 'xl/pivotCache/_rels/pivotCacheDefinition1.xml.rels' });

        xml = pivotTableXform.toXml(pivotTable);
        zip.append(xml, { name: 'xl/pivotTables/pivotTable1.xml' });

        xml = relsXform.toXml([
            {
                Id: 'rId1',
                Type: XLSX.RelType.PivotCacheDefinition,
                Target: '../pivotCache/pivotCacheDefinition1.xml',
            },
        ]);
        zip.append(xml, { name: 'xl/pivotTables/_rels/pivotTable1.xml.rels' });
    }

    async addContentTypes(zip, model) {
        const xform = new ContentTypesXform();
        const xml = xform.toXml(model);
        zip.append(xml, { name: '[Content_Types].xml' });
    }

    async addApp(zip, model) {
        const xform = new AppXform();
        const xml = xform.toXml(model);
        zip.append(xml, { name: 'docProps/app.xml' });
    }

    async addCore(zip, model) {
        const coreXform = new CoreXform();
        zip.append(coreXform.toXml(model), { name: 'docProps/core.xml' });
    }

    async addThemes(zip, model) {
        const themes = model.themes || { theme1: theme1Xml };
        Object.keys(themes).forEach(name => {
            const xml = themes[name];
            const path = `xl/theme/${name}.xml`;
            zip.append(xml, { name: path });
        });
    }

    async addOfficeRels(zip) {
        const xform = new RelationshipsXform();
        const xml = xform.toXml([
            { Id: 'rId1', Type: XLSX.RelType.OfficeDocument, Target: 'xl/workbook.xml' },
            { Id: 'rId2', Type: XLSX.RelType.CoreProperties, Target: 'docProps/core.xml' },
            { Id: 'rId3', Type: XLSX.RelType.ExtenderProperties, Target: 'docProps/app.xml' },
        ]);
        zip.append(xml, { name: '_rels/.rels' });
    }

    async addWorkbookRels(zip, model) {
        let count = 1;
        const relationships = [
            { Id: `rId${count++}`, Type: XLSX.RelType.Styles, Target: 'styles.xml' },
            { Id: `rId${count++}`, Type: XLSX.RelType.Theme, Target: 'theme/theme1.xml' },
        ];
        if (model.sharedStrings.count) {
            relationships.push({
                Id: `rId${count++}`,
                Type: XLSX.RelType.SharedStrings,
                Target: 'sharedStrings.xml',
            });
        }
        if ((model.pivotTables || []).length) {
            const pivotTable = model.pivotTables[0];
            pivotTable.rId = `rId${count++}`;
            relationships.push({
                Id: pivotTable.rId,
                Type: XLSX.RelType.PivotCacheDefinition,
                Target: 'pivotCache/pivotCacheDefinition1.xml',
            });
        }
        model.worksheets.forEach(worksheet => {
            worksheet.rId = `rId${count++}`;
            relationships.push({
                Id: worksheet.rId,
                Type: XLSX.RelType.Worksheet,
                Target: `worksheets/sheet${worksheet.id}.xml`,
            });
        });
        const xform = new RelationshipsXform();
        const xml = xform.toXml(relationships);
        zip.append(xml, { name: 'xl/_rels/workbook.xml.rels' });
    }

    async addSharedStrings(zip, model) {
        if (model.sharedStrings && model.sharedStrings.count) {
            zip.append(model.sharedStrings.xml, { name: 'xl/sharedStrings.xml' });
        }
    }

    async addStyles(zip, model) {
        const { xml } = model.styles;
        if (xml) {
            zip.append(xml, { name: 'xl/styles.xml' });
        }
    }

    async addWorkbook(zip, model) {
        const xform = new WorkbookXform();
        zip.append(xform.toXml(model), { name: 'xl/workbook.xml' });
    }

    async addWorksheets(zip, model) {
        // preparation phase
        const worksheetXform = new WorksheetXform();
        const relationshipsXform = new RelationshipsXform();
        const commentsXform = new CommentsXform();
        const vmlNotesXform = new VmlNotesXform();

        // write sheets
        model.worksheets.forEach(worksheet => {
            let xmlStream = new XmlStream();
            worksheetXform.render(xmlStream, worksheet);
            zip.append(xmlStream.xml, { name: `xl/worksheets/sheet${worksheet.id}.xml` });

            if (worksheet.rels && worksheet.rels.length) {
                xmlStream = new XmlStream();
                relationshipsXform.render(xmlStream, worksheet.rels);
                zip.append(xmlStream.xml, { name: `xl/worksheets/_rels/sheet${worksheet.id}.xml.rels` });
            }

            if (worksheet.comments.length > 0) {
                xmlStream = new XmlStream();
                commentsXform.render(xmlStream, worksheet);
                zip.append(xmlStream.xml, { name: `xl/comments${worksheet.id}.xml` });

                xmlStream = new XmlStream();
                vmlNotesXform.render(xmlStream, worksheet);
                zip.append(xmlStream.xml, { name: `xl/drawings/vmlDrawing${worksheet.id}.vml` });
            }
        });
    }

    _finalize(zip) {
        return new Promise(async (resolve, reject) => {
            try {
                await zip.finalize();
            } catch (e) {
                reject(e)
            }
            resolve(this);
        });
    }

    prepareModel(model, options) {
        // ensure following properties have sane values
        model.creator = model.creator || 'ExcelJS';
        model.lastModifiedBy = model.lastModifiedBy || 'ExcelJS';
        model.created = model.created || new Date();
        model.modified = model.modified || new Date();

        model.useSharedStrings = options.useSharedStrings !== undefined ? options.useSharedStrings : true;
        model.useStyles = options.useStyles !== undefined ? options.useStyles : true;

        // Manage the shared strings
        model.sharedStrings = new SharedStringsXform();

        // add a style manager to handle cell formats, fonts, etc.
        model.styles = model.useStyles ? new StylesXform(true) : new StylesXform.Mock();

        // prepare all of the things before the render
        const workbookXform = new WorkbookXform();
        const worksheetXform = new WorksheetXform();

        workbookXform.prepare(model);

        const worksheetOptions = {
            sharedStrings: model.sharedStrings,
            styles: model.styles,
            date1904: model.properties.date1904,
            drawingsCount: 0,
            media: model.media,
        };
        worksheetOptions.drawings = model.drawings = [];
        worksheetOptions.commentRefs = model.commentRefs = [];
        let tableCount = 0;
        model.tables = [];
        model.worksheets.forEach(worksheet => {
            // assign unique filenames to tables
            worksheet.tables.forEach(table => {
                tableCount++;
                table.target = `table${tableCount}.xml`;
                table.id = tableCount;
                model.tables.push(table);
            });

            worksheetXform.prepare(worksheet, worksheetOptions);
        });

        // TODO: workbook drawing list
    }

    async write(stream, options) {
        try {
            options = options || {};
            const { model } = this.workbook;
            const zip = new ZipStream.ZipWriter(options.zip);
            zip.pipe(stream);
            // render
            this.prepareModel(model, options);
            await this.addContentTypes(zip, model);
            await this.addOfficeRels(zip, model);
            await this.addWorkbookRels(zip, model);
            await this.addWorksheets(zip, model);
            await this.addSharedStrings(zip, model); // always after worksheets
            await this.addDrawings(zip, model);
            await this.addTables(zip, model);
            await this.addPivotTables(zip, model);
            await Promise.all([this.addThemes(zip, model), this.addStyles(zip, model)]);
            await this.addMedia(zip, model);
            await Promise.all([this.addApp(zip, model), this.addCore(zip, model)]);
            await this.addWorkbook(zip, model);

            return this._finalize(zip);
        } catch (e) {
            LogUtil.error("write xlsx error:" + e.message)
        }
    }

    writeFile(filename, options) {
        const stream = fs.createStreamSync(filename, "w+");
        return new Promise((resolve, reject) => {
            this.write(stream, options)
                .then(() => {
                    resolve();
                }).catch(err => {
                LogUtil.error("writeFile error:" + JSON.stringify(err))
                reject(err);
            }).finally(() => {
                stream.closeSync();
            })
        });
    }

    async writeBuffer(options) {
        try {
            const stream = new StreamBuf();
            await this.write(stream, options);
            return stream.read();
        } catch (e) {
            LogUtil.error("writeBuffer error:" + JSON.stringify(e))
        }

    }
}

XLSX.RelType = RelType;

export default XLSX;
